身為一個需要跟資料庫打交道的框架,怎麼可以在Model這邊草草結束呢!今天繼續來深入Model
我們今天會提到以下重點:
多資料庫可能會有幾種狀況:
以Django的角度來說,在技術上都是可行的
通常資料量非常大需要分表或是多租戶可能會用到第一點,需要維持查表效率或是隔離數據。第二種可以使app的操作更有彈性,但是配置上需要更加小心。使用第三種的話較為單純,這邊也示範第三種方法
python3 manage.py startapp chat
# settings.py
INSTALLED_APPS = [
    ...
    # my apps
    "article",
    "chat",
]
class Chat(models.Model):
    chat_id = models.AutoField(primary_key=True)
    message = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    user = models.IntegerField()
    class Meta:
        app_label = "chat"
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": DB_NAME,
        "USER": DB_USER,
        "PASSWORD": DB_PWD,
        "HOST": DB_HOST,
        "PORT": "5432",
    },
    "second_db": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": SECOND_DB_NAME,
        "USER": SECOND_DB_USER,
        "PASSWORD": SECOND_DB_PWD,
        "HOST": SECOND_DB_HOST,
        "PORT": "5432",
    },
}
接下來還不能馬上進行遷移,因為makemigrations只會確認default下的DB,因此我們需要配置DB的路由
dbRouter.py
from django.conf import settings
class DatabaseRouter:
    def db_for_read(self, model, **hints):
        if model._meta.app_label == "article":
            return "default"
        elif model._meta.app_label == "chat":
            return "second_db"
        else:
            return "default"
    def db_for_write(self, model, **hints):
        if model._meta.app_label == "article":
            return "default"
        elif model._meta.app_label == "chat":
            return "second_db"
        else:
            return "default"
    def allow_relation(self, obj1, obj2, **hints):
        """
        決定是否允許兩個對象之間建立關係。
        對於跨數據庫的關係,我們需要更精確的控制。
        """
        # 如果兩個對象在同一個數據庫,允許關係
        if obj1._state.db == obj2._state.db:
            return True
        # 其他跨數據庫的關係默認不允許
        return False
    def allow_migrate(self, db, app_label, model_name=None, **hints):
        """
        表示是否允許模型在指定資料庫上進行遷移操作
        """
        if app_label == "article":
            return db == "default"
        elif app_label == "chat":
            return db == "second_db"
        elif app_label == "auth":
            return db == "default"
        return None
settings.py中配置DATABASE_ROUTERS = ["django_project.dbRouter.DatabaseRouter"]
python manage.py makemigrations chat
python manage.py migrate chat --database=second_db
# shell 模式
>>> from chat.models import Chat
>>> chat = Chat(message="", user=1)
>>> chat.save()
# 如果都沒有設定路由,也可以使用如下方式
>>> Author.objects.using("default")
>>> Chat.objects.using("second_db")
一般狀況下,增加model並且遷移到實際的DB中需要我們寫程式碼並且執行遷移指令。但是有時候我們可能會想要動態的添加表格,可能操作人員需要後台有這樣的功能,又或是有一些排程下的資料需要不斷建立表格來輸入資料,此時能動態添加表格就是很重要的功能了。當然在執行操作之前,一定要確保相關的安全措施與用戶的權限設置等等
這邊加上type hint是更方便理解整個流程的運作
from django.db import models
from typing import Dict, Any, Optional, Type, Callable
def create_model(
    name: str,
    fields: Optional[Dict[str, Any]] = None,
    app_label: str = "",
    options: Optional[Dict[str, Any]] = None,
) -> Type[models.Model]:
    """
    創建模型對象
    :param name: 模型名稱
    :param fields: 模型欄位
    :param app_label: 應用名稱
    :param options: 模型選項
    :return: 模型對象
    """
    class Meta:
        pass
    setattr(Meta, "app_label", app_label)
    # 設置模型的meta選項
    if options is not None:
        for key, value in options.items():
            setattr(Meta, key, value)
    # 設置模型的欄位
    attrs: Dict[str, Any] = {"__module__": f"{app_label}.models", "Meta": Meta}
    if fields:
        attrs.update(fields)
    # 創建模型
    model: Type[models.Model] = type(name, (models.Model,), attrs)
    return model
def create_db(model: Type[models.Model]) -> None:
    """
    創建資料表
    :param model: 模型對象
    """
    from django.db import connection
    from django.db.backends.base.schema import BaseDatabaseSchemaEditor
    try:
        with BaseDatabaseSchemaEditor(connection) as schema_editor:
            schema_editor.create_model(model=model)
    except Exception as e:
        pass
def create_table(model_name: str) -> Type[models.Model]:
    """
    創建資料表
    :param model_name: 模型名稱
    :return: 模型對象
    """
    fields: Dict[str, Any] = {
        "id": models.AutoField(primary_key=True),
        "views": models.IntegerField(default=0),
        "date": models.DateField(auto_now_add=True),
        "article": models.ForeignKey("Article", on_delete=models.CASCADE),
        "__str__": lambda self: f"{self.article.title}_{self.date}_views",
    }
    options: Dict[str, str] = {
        "verbose_name": model_name,
        "verbose_name_plural": model_name,
    }
    model: Type[models.Model] = create_model(
        name=model_name, fields=fields, app_label="article", options=options
    )
    create_db(model)
    return model
在create_model中,我們根據傳入的參數來建造模型對象,並且使 python內置的type()來建立
type(name, bases, dict)
這個函數有三個參數:
name: 這是要創建的類的名稱bases: 這是一個元組,包含新類要繼承的基類。(models.Model,),表示新創建的類將繼承 models.Model
dict: 這是一個字典,包含類的屬性和方法。 attrs 字典中包含了 __module__、Meta 以及所有的模型欄位而create_db則是讓我們能夠在資料庫中建立我們設置好的模型類,其中BaseDatabaseSchemaEditor是Django 用於執行數據庫架構更改的基礎類別
最後create_table則是將整個流程定下來,調用create_model後把model當作參數丟入create_db中,最後返回model對象提供後續的操作
實際操作一次
def create_table_view(request):
    today = time.localtime(time.time())
    article = Article.objects.get(article_id=2)
    model_name = f"{article.article_id}_{time.strftime('%Y%m%d', today)}_view"
    new_model = create_table(model_name=model_name)
    new_model.objects.create(
        views=0,
        date=today,
        article=article,
    )
    return JsonResponse({"status": "success"})
可以看到確實新增了表格以及載入了數據


我們介紹了兩樣比較進階的資料庫操作方式
這兩者都能讓我們在開發中,面臨到動態的需求變更等都能更靈活的進行調整,滿足各式各樣的需求
目前為止,有稍微感受到Django對於資料庫使用ORM操作的魅力了嗎?除了程式碼簡潔且可讀性高之外,透過自定義的路由設置,讓我們在多變的系統下不用去修改原先的ORM語法,可以說是相當便利~
到此有關Model的部分先告一個段落,下一次再來討論資料庫的結構就會是之後的多租戶架構了
到目前為止,我們添加數據都是透過shell模式下進行操作,明天開始我們會透過表單來進行數據的添加。有了Django表單,除了在數據驗證的部分更加方便,在視圖中的程式碼也會更加的簡潔與易讀,可以期待一下
多資料庫使用:https://docs.djangoproject.com/en/5.1/topics/db/multi-db/
SchemaEditor:https://docs.djangoproject.com/en/5.1/ref/schema-editor/